(ns scripts.cljmon.parser
  (:use util.mon
        util.file
        util.utils)
  (:require scripts.cljmon.csv
            scripts.cljmon.json)
  (:require clojure.string))

(def ^:dynamic *range* :auto)

(defn- parse-log-lines [log]
  (if (instance? StringBuilder log)
    (clojure.string/split-lines (str log))
    (file-lines-all log)))

;;; analysis

(defn- avg [col]
  (/ (apply + col) (count col)))

(defn- avg-duration-raw [records]
  (if (empty? records)
    {:cpu 0.0 :duration 0}
    (let [times (map first records)
          cpus (map second records)]
      {:cpu (/ (apply + cpus) (count cpus))
       :duration (quot (- (last times) (first times)) 1000)})))

(def ^:dynamic *bottom-working-cpu* 3)

(defn- working-cpu [records]
  (reverse
    (drop-while #(< (second %) *bottom-working-cpu*)
      (reverse
        (drop-while #(< (second %) *bottom-working-cpu*) records)))))

(defn- ranged-cpu [records]
  (let [[start end] *range*]
    (filter #(<= start (first %) end) records)))

(defn- select-cpu [records]
  (if (vector? *range*)
    (ranged-cpu records)
    (case *range*
      :all records
      :auto (working-cpu records))))

(defn- avg-duration [records]
  (if (vector? *range*)
    (avg-duration-raw (ranged-cpu records))
    (case *range*
      :all (avg-duration-raw records)
      :auto (let [times (map first records)
                  cpus (map second records)]
              (if (or (nil? *bottom-working-cpu*) (zero? *bottom-working-cpu*) (< (apply max cpus) (* *bottom-working-cpu* 1.5)))
                (avg-duration-raw records)
                (avg-duration-raw (working-cpu records)))))))

(defn- check-format [lines]
  (if (= (first (first lines)) \{)
    :json
    :csv))

(defn parse-comments [logfile]
  (let [lines (parse-log-lines logfile)
        comments ((case (check-format lines) :csv scripts.cljmon.csv/parse-comments :json scripts.cljmon.json/parse-comments) lines)
        processes (filter #(re-matches #"(host: \S+, )?pid: \d+.*" %) comments)
        networks (filter #(re-matches #"Network Interface NI\d+ ->.*" %) comments)]
    {:process (into {} (for [p processes] (vec (let [[_ host pid label] (re-matches #"(?:host: (\S+), )?pid: (\d+), name: [^,]+(?:, label: ([^,]+))?.*" p) label (or label (Integer/parseInt pid))] [(if host [host label] label) p]))))
     :network (into {} (for [n networks] (vec (next (re-matches #"Network Interface (NI\d+) ->\s*(.*)" n)))))}))

(defn parse-processes-from-comments [{:keys [process]}]
  (into {}
    (for [[k v] process]
      [k (into {}
           (for [[_ k v] (re-seq #"\b(\w+): ([^,]*)" v)]
             [(keyword k) v]))])))

(defn parse-log [logfile]
  (let [lines (parse-log-lines logfile)
        data ((case (check-format lines) :csv scripts.cljmon.csv/parse-records :json scripts.cljmon.json/parse-records) lines)
        comments (parse-comments logfile)]
    (assoc data :comments comments :processes (parse-processes-from-comments comments))))

(defn parse-multi-logs [logs]
  (let [results (map parse-log logs)
        headers (vec (distinct (apply concat (map :headers results))))
        named-records (apply concat (map :named-records results))]
    {:headers headers
     :records (map (fn [r] (map #(get r %) headers)) named-records)
     :named-records named-records
     :columns (map list headers (range))
     :comments (apply merge-with merge (map :comments results))
     :processes (apply merge (map :processes results))}))


(defn- system-duration-range [logfile] ; FIXME: duplicated code
  (let [{:keys [headers records named-records columns comments]} (parse-log logfile)
        system-column (second (first (filter #(= (first %) "System CPU") columns)))]
    (if system-column
      (let [records (select-cpu
                      (map #(vector (Long/parseLong (first %)) ; time
                              (Double/parseDouble (nth % system-column))) ; cpu
                        (take-while #(not= (nth % system-column) "-") records)))]
        [(first (first records)) (first (last records))])
      [(first (first records)) (first (last records))])))

(defn annotate-process-name [stuffs processes]
  (into {}
    (for [[pid info] stuffs]
      [pid (if-let [name (:name (get processes pid))]
             (assoc info :name name)
             info)])))

(defn read-cpu [logfile]
  (let [{:keys [headers records named-records columns comments processes]} (parse-log logfile)
        cpu-column-header-regexp #"(.+) CPU(?: @ (\S+))?$"
        cpu-column-headers (filter #(re-matches cpu-column-header-regexp (first %)) columns)
        cpus (into {} (for [[k i] cpu-column-headers]
                        [k (avg-duration
                             (map #(vector (Long/parseLong (first %)) ; time
                                     (Double/parseDouble (nth % i))) ; cpu
                               (take-while #(and (not= (nth % i) "-") (not= (nth % i) "NaN")) records)))]))
        cpu-header-mapping (fn [p] (cond
                                     (= p "System") :system
                                     (re-matches #"\d+" p) (Long/parseLong p)
                                     :else p))
        cpus (into {} (for [[k v] cpus]
                        (let [[_ pid host] (re-matches cpu-column-header-regexp k)
                              pid (cond (= pid "System") :system (re-matches #"\d+" pid) (Long/parseLong pid) :else pid)
                              k (if host
                                  (if (keyword? pid)
                                    (keyword (str (name pid) "@" host))
                                    (str pid "@" host))
                                  pid)]
                          [k v])))]
    (annotate-process-name cpus processes)))

(defn read-system-cpu [logfile]
  (get (read-cpu logfile) :system))

(defn read-pid-cpu [logfile pid]
  (get (read-cpu logfile) (long pid)))

(defn display-cpu [cpus]
  (doseq [[field {:keys [cpu duration name]}] cpus]
    (println (format "%s: %.02f%%, %d seconds"
               (cond
                 (= :system field) "System CPU"
                 (and (integer? field) name) (format "%d/%s CPU" field name)
                 (integer? field) (str field " CPU")
                 :else field)
               cpu duration))))

(defn read-memory [logfile]
  (let [{:keys [headers records named-records columns comments processes]} (parse-log logfile)
        memory-column-header-regexp #"(.+) (VM|Memory|Memory Used|Memory Free)(?: \((\w+)\))?(?: @ (\S+))?"
        mem-column-headers (filter #(re-matches memory-column-header-regexp (first %)) columns)
        mems (for [[k i] mem-column-headers]
               (let [[_ pid mem-type unit host] (re-matches memory-column-header-regexp k)
                     pid (cond (= pid "System") :system (re-matches #"\d+" pid) (Long/parseLong pid) :else pid)
                     k (if host
                         (if (keyword? pid)
                           (keyword (str (name pid) "@" host))
                           (str pid "@" host))
                         pid)
                     mem-type (condp = mem-type "VM" :vm "Memory" :memory "Memory Used" :used "Memory Free" :free)
                     size (first (remove #(= % "-") (map #(nth % i) (reverse records))))]
                 [k mem-type size unit host]))
        mems (into {} (for [[pid v] (group-by-first mems)]
                        [pid (assoc (into {} (map #(vector (nth % 1) (nth % 2)) v)) :unit (nth (first v) 3))]))]
    (annotate-process-name mems processes)))

(defn display-memory [mems]
  (doseq [[pid {m :memory vm :vm used :used free :free u :unit name :name}] mems]
    (if-not (= pid :system)
      (println (format "%s/%s: Memory %s (%s), Virtual Memory %s (%s)" pid name m u vm u))
      (println (format "System: Used Memory %s (%s), Free Memory %s (%s)" used u free u)))))

(defn read-disk-io [logfile]
  (let [[start end] (system-duration-range logfile)]
    (when start
      (let [{:keys [headers records named-records columns]} (parse-log logfile)]
        (if-let [[column-title column-index] (first (filter #(re-matches #"Disk IO \(\w+\)" (first %)) columns))]
          (let [[_ unit] (re-matches #"Disk IO \((\w+)\)" column-title)
                records (map #(vector (Long/parseLong (first %)) (Integer/parseInt (nth % column-index))) records)
                records (drop-while #(< (first %) start) (take-while #(<= (first %) end) records))
                records (map second records)]
            {:io (quot (apply + records) (count records)) :unit unit})
          {:io "-"})))))

(defn display-disk-io [{:keys [io unit] :or {io "-"}}]
  (if unit
    (println (format "Disk IO: %s %s/s" io unit))
    (println (format "Disk IO: %s" io))))


(defn read-network-io [logfile] ; TODO: support remote
  (let [[start end] (system-duration-range logfile)]
    (when start
      (let [{:keys [headers records named-records columns]} (parse-log logfile)]
        (if-let [[column-title column-index] (first (filter #(re-matches #"Network IO \(\w+\)" (first %)) columns))]
          (let [[_ unit] (re-matches #"Network IO \((\w+)\)" column-title)
                records (map #(vector (Long/parseLong (first %)) (Integer/parseInt (nth % column-index))) records)
                records (drop-while #(< (first %) start) (take-while #(<= (first %) end) records))
                records (map second records)]
            {:io (quot (apply + records) (count records)) :unit unit})
          {:io "-"})))))

(defn display-network-io [{:keys [io unit] :or {io "-"}}] ; TODO: support remote
  (if unit
    (println (format "Network IO: %s %s/s" io unit))
    (println (format "Network IO: %s" io))))


